﻿// -----------------------------------------------------------------------
// <copyright file="ActiveDirectoryDataProvider.cs" company="EPAM Systems">
////Copyright (c) EPAM Systems, 2012. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

namespace MBOP.DataAccess.DataProviders
{
	#region Usings

	using System;
	using System.Collections.Generic;
	using System.DirectoryServices;
	using System.DirectoryServices.AccountManagement;
	using System.DirectoryServices.ActiveDirectory;

	using MBOP.Core.DTO;
	using MBOP.Core.Entities.Common;
	using MBOP.Core.Interfaces.DataProviders;
	using MBOP.Helpers.Extensions;

	#endregion

	/// <summary>
	/// The <see cref="IActiveDirectoryDataProvider"/> implementer
	/// which provides data from active directory.
	/// </summary>
	public class ActiveDirectoryDataProvider : IActiveDirectoryDataProvider
	{
		#region Constants

		/// <summary>
		/// Represents the default priority domain where to search in.
		/// </summary>
		private const string DEFAULT_PRIORITY_DOMAIN = "minsk.epam.com";

		/// <summary>
		/// Represents the max finding people count.
		/// </summary>
		private const int MAX_PERSON_COUNT = 8;

		/// <summary>
		/// Represents the name of the photo property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_PHOTO = "thumbnailPhoto";

		/// <summary>
		/// Represents the name of the email property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_EMAIL = "mail";

		/// <summary>
		/// Represents the name of the position property in active directory. 
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_POSITION = "Title";

		/// <summary>
		/// Represents the common name of the person name property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_COMMON_NAME = "CN";

		/// <summary>
		/// Represents the identity guid of the person name property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_IDENTITY = "objectGUID";

		/// <summary>
		/// Represents the sam account name property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_SAM_ACCOUNT_NAME = "sAMAccountName";

		/// <summary>
		/// Represents the given name of the person name property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_GIVEN_NAME = "givenName";

		/// <summary>
		/// Represents the name of the work address property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_ADDRESS = "physicalDeliveryOfficeName";

		/// <summary>
		/// Represents the name of the VOIP property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_VOIP = "telephoneNumber";

		/// <summary>
		/// Represents the name of the mobile phone property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_MOBILE = "mobile";

		/// <summary>
		/// Represents the name of the city property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_CITY = "company";

		/// <summary>
		/// Represents the name of the country property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_COUNTRY = "c";

		/// <summary>
		/// Represents the name of workstation property in active directory.
		/// </summary>
		private const string ACTIVE_DIRECTORY_PROPERTY_WORKSTATION = "postalCode";

		/// <summary>
		/// Represents the default location.
		/// </summary>
		private const string DEFAULT_LOCATION = "Mogilev";

		/// <summary>
		/// Represents the default mime type of the image.
		/// </summary>
		private const string DEFAULT_IMAGE_MIME_TYPE = "image/jpg";

		#endregion

		#region Public Methods

		/// <summary>
		/// Gets the person information.
		/// </summary>
		/// <param name="personFindingInfo">The person's finding info.</param>
		/// <param name="findingType">The finding type.</param>
		/// <param name="isImageInclude">Indicates image including.</param>
		/// <returns>The person info.</returns>
		public PersonInfo GetPersonInfo(string personFindingInfo, PersonFindingType findingType, bool isImageInclude)
		{
			IdentityType identityType = GetIdentityType(findingType);

			Stack<string> domains = GetDomainsList();

			bool isUserFound = false;

			PersonInfo personInfo = null;

			while (domains.Count != 0 && !isUserFound)
			{
				string currentDomain = domains.Pop();

				using (PrincipalContext context = new PrincipalContext(ContextType.Domain, currentDomain))
				{
					UserPrincipal user = UserPrincipal.FindByIdentity(context, identityType, personFindingInfo);

					if (user != null)
					{
						DirectoryEntry directoryEntry = user.GetUnderlyingObject() as DirectoryEntry;

						personInfo = GetPersonInfoFromDirectoryEntry(directoryEntry, isImageInclude);

						isUserFound = true;
					}
				}
			}

			return personInfo;
		}

		/// <summary>
		/// Gets the person list specified by begin name sequence.
		/// </summary>
		/// <param name="nameSequence">The begin name sequence.</param>
		/// <param name="isRestricted">The location restricted indicator.</param>
		/// <returns>The person list.</returns>
		public IEnumerable<PersonInfo> GetPersonList(string nameSequence, bool isRestricted)
		{
			Stack<string> domains = GetDomainsList();

			string clearedFilter = RemoveSpecialsFilterChars(nameSequence);

			IList<PersonInfo> personCollection = null;

			if (isRestricted)
			{
				personCollection = GetPersonList(clearedFilter, new string[] { domains.Peek() }, DEFAULT_LOCATION);
			}
			else
			{
				personCollection = GetPersonList(clearedFilter, domains, "*");
			}

			return personCollection;
		}

		#endregion

		#region Private Methods

		/// <summary>
		/// Checks that the current collection interval contains same elements.
		/// </summary>
		/// <param name="personList">The person collection.</param>
		/// <param name="startIndex">The start index.</param>
		/// <param name="endIndex">The last index.</param>
		/// <returns>The result of the checking operation.</returns>
		private static bool IsSameElements(List<PersonInfo> personList, int startIndex, int endIndex)
		{
			bool isAllPersonSame = true;

			for (int index = startIndex; index < endIndex - 1; index++)
			{
				if (personList[index].CompareTo(personList[index + 1]) != 0)
				{
					isAllPersonSame = false;
					break;
				}
			}

			return isAllPersonSame;
		}

		/// <summary>
		/// Gets the image from byte array.
		/// </summary>
		/// <param name="imageData">The image data</param>
		/// <returns>The image.</returns>
		private static Image GetImageFromData(byte[] imageData)
		{
			Image image = null;

			if (imageData != null)
			{
				image = new Image();

				Data data = new Data()
				{
					Content = imageData,
					ContentLength = imageData.Length,
					ContentType = DEFAULT_IMAGE_MIME_TYPE
				};

				image.Data = data;
			}

			return image;
		}

		/// <summary>
		/// Gets the image from directory entry.
		/// </summary>
		/// <param name="directoryEntry">The directory entry.</param>
		/// <returns>The image.</returns>
		private static Image GetImageFromDirectoryEntry(DirectoryEntry directoryEntry)
		{
			byte[] imageData = (byte[])directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_PHOTO].Value;

			return GetImageFromData(imageData);
		}

		/// <summary>
		/// Gets the person info from directory entry.
		/// </summary>
		/// <param name="directoryEntry">The directory entry.</param>
		/// <param name="isImageInclude">Indicates image including.</param>
		/// <returns>The person info.</returns>
		private static PersonInfo GetPersonInfoFromDirectoryEntry(DirectoryEntry directoryEntry, bool isImageInclude)
		{
			PersonInfo personInfo = new PersonInfo();

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_IDENTITY].Value != null)
			{
				personInfo.ID = new Guid((byte[])directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_IDENTITY].Value).ToString();
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_PHOTO].Value != null && isImageInclude)
			{
				personInfo.Photo = GetImageFromDirectoryEntry(directoryEntry);
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_EMAIL].Value != null)
			{
				personInfo.Email = (string)directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_EMAIL].Value;
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_POSITION].Value != null)
			{
				personInfo.Position = (string)directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_POSITION].Value;
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_COMMON_NAME].Value != null)
			{
				personInfo.CommonName = (string)directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_COMMON_NAME].Value;
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_GIVEN_NAME].Value != null)
			{
				personInfo.GivenName = (string)directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_GIVEN_NAME].Value;
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_ADDRESS].Value != null)
			{
				personInfo.DeliveryOffice = (string)directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_ADDRESS].Value;
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_SAM_ACCOUNT_NAME].Value != null)
			{
				personInfo.SamAccountName = (string)directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_SAM_ACCOUNT_NAME].Value;
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_CITY].Value != null)
			{
				personInfo.City = (string)directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_CITY].Value;
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_COUNTRY].Value != null)
			{
				personInfo.Country = (string)directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_COUNTRY].Value;
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_WORKSTATION].Value != null)
			{
				personInfo.Workstation = (string)directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_WORKSTATION].Value;
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_VOIP].Value != null)
			{
				int voipID;

				if (int.TryParse(directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_VOIP].Value.ToString(), out voipID))
				{
					personInfo.VOIPID = voipID;
				}
				else
				{
					personInfo.VOIPID = 0;
				}
			}

			if (directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_MOBILE].Value != null)
			{
				personInfo.Mobile = (string)directoryEntry.Properties[ACTIVE_DIRECTORY_PROPERTY_MOBILE].Value;
			}

			return personInfo;
		}

		/// <summary>
		/// Gets identity type from current finding type.
		/// </summary>
		/// <param name="findingType">The finding type.</param>
		/// <returns>The identity type.</returns>
		private static IdentityType GetIdentityType(PersonFindingType findingType)
		{
			IdentityType identityResult;

			switch (findingType)
			{
				case PersonFindingType.CommonName:

					identityResult = IdentityType.Name;

					break;

				case PersonFindingType.ID:

					identityResult = IdentityType.Guid;

					break;

				case PersonFindingType.LogonIdentity:

					identityResult = IdentityType.SamAccountName;

					break;

				default:

					identityResult = IdentityType.Guid;

					break;
			}

			return identityResult;
		}

		/// <summary>
		/// Gets persons from active directory by specifies domains and location.
		/// </summary>
		/// <param name="nameSequence">The name sequence</param>
		/// <param name="domains">The domains where is searching for.</param>
		/// <param name="cityFilter">The city filter.</param>
		/// <returns>The person list.</returns>
		private static IList<PersonInfo> GetPersonList(string nameSequence, IEnumerable<string> domains, string cityFilter)
		{
			List<PersonInfo> foundPersons = new List<PersonInfo>();

			foreach (var domain in domains)
			{
				using (PrincipalContext context = new PrincipalContext(ContextType.Domain, domain))
				{
					UserPrincipal qbeUser = new UserPrincipal(context);

					DirectoryEntry directoryEntry = new DirectoryEntry("LDAP://" + domain);

					DirectorySearcher searcher = new DirectorySearcher(directoryEntry);

					searcher.Filter = string.Concat("(&(objectClass=user)(|(cn=", nameSequence, "*)", "(sn=", nameSequence, "*))", "(&(company=", cityFilter, ")))");

					searcher.SizeLimit = MAX_PERSON_COUNT;

					SearchResultCollection resultCollection = searcher.FindAll();

					foreach (SearchResult searchResult in resultCollection)
					{
						PersonInfo currentPerson = GetPersonInfoFromDirectoryEntry(searchResult.GetDirectoryEntry(), false);

						foundPersons.Add(currentPerson);
					}
				}
			}

			foundPersons.Sort((lhsPerson, rhsPerson) => lhsPerson.CompareTo(rhsPerson));

			List<PersonInfo> result = new List<PersonInfo>();

			if (foundPersons.Count > MAX_PERSON_COUNT)
			{
				bool isPersonRangeSame = IsSameElements(foundPersons, 0, MAX_PERSON_COUNT);

				if (!isPersonRangeSame)
				{
					for (int index = 0; index < MAX_PERSON_COUNT; index++)
					{
						result.Add(foundPersons[index]);
					}
				}
			}

			if (result.Count == 0)
			{
				result = foundPersons;
			}

			return result;
		}

		/// <summary>
		/// Gets the domain list from the forest.
		/// </summary>
		/// <returns>The domain list.</returns>
		private static Stack<string> GetDomainsList()
		{
			Stack<string> domainsList = new Stack<string>();

			bool hasPriorityDomain = false;

			using (Forest currentForest = Forest.GetForest(new DirectoryContext(DirectoryContextType.Forest, "epam.com")))
			{
				foreach (Domain childrenDomain in currentForest.RootDomain.Children)
				{
					if (childrenDomain.Name != DEFAULT_PRIORITY_DOMAIN)
					{
						domainsList.Push(childrenDomain.Name);
					}
					else
					{
						hasPriorityDomain = true;
					}
				}

				if (hasPriorityDomain)
				{
					domainsList.Push(DEFAULT_PRIORITY_DOMAIN);
				}
			}

			return domainsList;
		}

		/// <summary>
		/// Removes specials chars for filtering.
		/// </summary>
		/// <param name="input">The input string</param>
		/// <returns>The converted string.</returns>
		private static string RemoveSpecialsFilterChars(string input)
		{
			return input.RemoveSpecialChars(new char[] { '*', '(', ')', '=', ',' });
		}

		#endregion
	}
}